
/*-------------------------------------------------------------------------
 *
 * spginsert.c
 *	  对外可见的索引创建/插入例程
 *
 * 所有实际的插入逻辑在 spgdoinsert.c 中。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *			src/backend/access/spgist/spginsert.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/genam.h"
#include "access/spgist_private.h"
#include "access/spgxlog.h"
#include "access/tableam.h"
#include "access/xlog.h"
#include "access/xloginsert.h"
#include "catalog/index.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
#include "storage/smgr.h"
#include "utils/memutils.h"
#include "utils/rel.h"


typedef struct
{
	SpGistState spgstate;		/* SPGiST的工作状态 */
	int64		indtuples;		/* 已索引的元组总数 */
	MemoryContext tmpCtx;		/* 每个元组的临时上下文 */
} SpGistBuildState;


/* 在 table_index_build_scan 中处理一个堆元组的回调 */
static void fc_spgistBuildCallback(Relation fc_index, ItemPointer fc_tid, Datum *fc_values,
					bool *fc_isnull, bool fc_tupleIsAlive, void *fc_state)
{
	SpGistBuildState *fc_buildstate = (SpGistBuildState *) fc_state;
	MemoryContext fc_oldCtx;

	/* 在临时上下文中工作，并在每个元组后重置它 */
	fc_oldCtx = MemoryContextSwitchTo(fc_buildstate->tmpCtx);

	/*
	 * 尽管没有并发插入可以发生，但我们仍可能
	 * 因为 bgwriter 或检查点在某些缓冲区上获取锁而导致缓冲区锁定失败。
	 * 所以我们需要准备重试。当重试时我们可以刷新
	 * 任何临时数据。
	 */
	while (!spgdoinsert(fc_index, &fc_buildstate->spgstate, fc_tid,
						fc_values, fc_isnull))
	{
		MemoryContextReset(fc_buildstate->tmpCtx);
	}

	/* 更新总元组计数 */
	fc_buildstate->indtuples += 1;

	MemoryContextSwitchTo(fc_oldCtx);
	MemoryContextReset(fc_buildstate->tmpCtx);
}

/*
 * 构建一个 SP-GiST 索引。
 */
IndexBuildResult * spgbuild(Relation fc_heap, Relation fc_index, IndexInfo *fc_indexInfo)
{
	IndexBuildResult *fc_result;
	double		fc_reltuples;
	SpGistBuildState fc_buildstate;
	Buffer		fc_metabuffer,
				fc_rootbuffer,
				fc_nullbuffer;

	if (RelationGetNumberOfBlocks(fc_index) != 0)
		elog(ERROR, "index \"%s\" already contains data",
			 RelationGetRelationName(fc_index));

	/*
	 * 初始化元数据页和根页
	 */
	fc_metabuffer = SpGistNewBuffer(fc_index);
	fc_rootbuffer = SpGistNewBuffer(fc_index);
	fc_nullbuffer = SpGistNewBuffer(fc_index);

	Assert(BufferGetBlockNumber(fc_metabuffer) == SPGIST_METAPAGE_BLKNO);
	Assert(BufferGetBlockNumber(fc_rootbuffer) == SPGIST_ROOT_BLKNO);
	Assert(BufferGetBlockNumber(fc_nullbuffer) == SPGIST_NULL_BLKNO);

	START_CRIT_SECTION();

	SpGistInitMetapage(BufferGetPage(fc_metabuffer));
	MarkBufferDirty(fc_metabuffer);
	SpGistInitBuffer(fc_rootbuffer, SPGIST_LEAF);
	MarkBufferDirty(fc_rootbuffer);
	SpGistInitBuffer(fc_nullbuffer, SPGIST_LEAF | SPGIST_NULLS);
	MarkBufferDirty(fc_nullbuffer);


	END_CRIT_SECTION();

	UnlockReleaseBuffer(fc_metabuffer);
	UnlockReleaseBuffer(fc_rootbuffer);
	UnlockReleaseBuffer(fc_nullbuffer);

	/*
	 * 现在将所有堆数据插入索引
	 */
	initSpGistState(&fc_buildstate.spgstate, fc_index);
	fc_buildstate.spgstate.isBuild = true;
	fc_buildstate.indtuples = 0;

	fc_buildstate.tmpCtx = AllocSetContextCreate(CurrentMemoryContext,
											  "SP-GiST build temporary context",
											  ALLOCSET_DEFAULT_SIZES);

	fc_reltuples = table_index_build_scan(fc_heap, fc_index, fc_indexInfo, true, true,
									   fc_spgistBuildCallback, (void *) &fc_buildstate,
									   NULL);

	MemoryContextDelete(fc_buildstate.tmpCtx);

	SpGistUpdateMetaPage(fc_index);

	/*
	 * 在构建索引时我们没有写WAL记录，因此如果需要WAL日志记录，
	 * 现在将所有页面写入WAL。
	 */
	if (RelationNeedsWAL(fc_index))
	{
		log_newpage_range(fc_index, MAIN_FORKNUM,
						  0, RelationGetNumberOfBlocks(fc_index),
						  true);
	}

	fc_result = (IndexBuildResult *) palloc0(sizeof(IndexBuildResult));
	fc_result->heap_tuples = fc_reltuples;
	fc_result->index_tuples = fc_buildstate.indtuples;

	return fc_result;
}

/*
 * 在初始化分叉中构建一个空的 SPGiST 索引
 */
void spgbuildempty(Relation fc_index)
{
	Page		fc_page;

	/* 构造元数据页。 */
	fc_page = (Page) palloc(BLCKSZ);
	SpGistInitMetapage(fc_page);

	/*
	 * 无条件地写入页面并记录。这一点很重要，
	 * 尤其是对在表空间和数据库上创建的索引
	 * 而其创建发生在上一个重做指针之后，因为恢复会移除当
	 * 重放对应创建记录时它们现有的任何内容。
	 */
	PageSetChecksumInplace(fc_page, SPGIST_METAPAGE_BLKNO);
	smgrwrite(RelationGetSmgr(fc_index), INIT_FORKNUM, SPGIST_METAPAGE_BLKNO,
			  (char *) fc_page, true);
	log_newpage(&(RelationGetSmgr(fc_index))->smgr_rnode.node, INIT_FORKNUM,
				SPGIST_METAPAGE_BLKNO, fc_page, true);

	/* 同样适用于根页。 */
	SpGistInitPage(fc_page, SPGIST_LEAF);

	PageSetChecksumInplace(fc_page, SPGIST_ROOT_BLKNO);
	smgrwrite(RelationGetSmgr(fc_index), INIT_FORKNUM, SPGIST_ROOT_BLKNO,
			  (char *) fc_page, true);
	log_newpage(&(RelationGetSmgr(fc_index))->smgr_rnode.node, INIT_FORKNUM,
				SPGIST_ROOT_BLKNO, fc_page, true);

	/* 同样适用于空元组根页。 */
	SpGistInitPage(fc_page, SPGIST_LEAF | SPGIST_NULLS);

	PageSetChecksumInplace(fc_page, SPGIST_NULL_BLKNO);
	smgrwrite(RelationGetSmgr(fc_index), INIT_FORKNUM, SPGIST_NULL_BLKNO,
			  (char *) fc_page, true);
	log_newpage(&(RelationGetSmgr(fc_index))->smgr_rnode.node, INIT_FORKNUM,
				SPGIST_NULL_BLKNO, fc_page, true);

	/*
	 * 需要立即同步，即使我们已经记录了页面，
	 * 因为写入并未经过共享缓冲区，因此并发
	 * 检查点可能已将重做指针移到我们的 xlog 记录之外。
	 */
	smgrimmedsync(RelationGetSmgr(fc_index), INIT_FORKNUM);
}

/*
 * 将一个新元组插入到 SPGiST 索引中。
 */
bool spginsert(Relation fc_index, Datum *fc_values, bool *fc_isnull,
		  ItemPointer fc_ht_ctid, Relation fc_heapRel,
		  IndexUniqueCheck fc_checkUnique,
		  bool fc_indexUnchanged,
		  IndexInfo *fc_indexInfo)
{
	SpGistState fc_spgstate;
	MemoryContext fc_oldCtx;
	MemoryContext fc_insertCtx;

	fc_insertCtx = AllocSetContextCreate(CurrentMemoryContext,
									  "SP-GiST insert temporary context",
									  ALLOCSET_DEFAULT_SIZES);
	fc_oldCtx = MemoryContextSwitchTo(fc_insertCtx);

	initSpGistState(&fc_spgstate, fc_index);

	/*
	 * 如果与并发插入发生冲突，我们可能需要多次重复 spgdoinsert()。
	 * 如果是这样，每次都重置 insertCtx，以避免累积的内存消耗。
	 * 这意味着我们还必须重新执行 initSpGistState()，但这足够便宜，不会造成问题。
	 */
	while (!spgdoinsert(fc_index, &fc_spgstate, fc_ht_ctid, fc_values, fc_isnull))
	{
		MemoryContextReset(fc_insertCtx);
		initSpGistState(&fc_spgstate, fc_index);
	}

	SpGistUpdateMetaPage(fc_index);

	MemoryContextSwitchTo(fc_oldCtx);
	MemoryContextDelete(fc_insertCtx);

	/* 返回 false 因为我们没有进行任何唯一性检查 */
	return false;
}
